/*
 * Alloy Analyzer
 * Copyright (c) 2002 Massachusetts Institute of Technology
 *
 * The Alloy Analyzer is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * The Alloy Analyzer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the Alloy Analyzer; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package alloy.gui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

import javax.swing.AbstractAction;
import javax.swing.DefaultCellEditor;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import javax.swing.text.View;

/** An extension to JTable which adds default rendering/editing behavior for certain classes.
 *  - Date objects show the date and time.
 *  - AnalysisIcon objects display tooltips.
 *
 *  Furthermore:
 *  - AlloyTable columns are automatically resized to fit the table data.
 *  - AlloyTable rows can be sorted by clicked on the appropriate column header.
 *  - The first row of an AlloyTable can be made to display different using setFirstRowIsTemp()
 */
class AlloyTable extends JTable {
    public static final int LEFT_CLIPPING = 1;
    public static final int RIGHT_CLIPPING = 2;

    private TableColumn column[];
    private JMenuItem columnMenuItems[];
    private boolean columnIsVisible[];
    private boolean columnIsFixedWidth[];
    private TableSorter sorter;
    private boolean firstRowIsTemp;
    private int columnClipping[];

    private javax.swing.Timer firstRowUpdateTimer;
    private boolean continuouslyUpdateFirstRow;
    private boolean canContinuouslyUpdate[];

    private double windowsScaleFactor =
    (double)Toolkit.getDefaultToolkit().getScreenResolution() / 72.0;

    private Font selectedFont, renderedFont;

    private static DateFormat dateFormatter =
	new SimpleDateFormat("MMM d, yyyy: h.mma");

    // for fixing "ugly fonts" on windows
    private static boolean onWindows =
        (System.getProperty("os.name").toUpperCase().indexOf("WINDOWS") > -1);

    AlloyTable(final TableModel tm) {
    super();

    if (AlloySwingUtilities.onMac()) {
        getTableHeader().setBorder(AlloySwingUtilities.createBottomBorder(Color.lightGray));
    }

    setShowVerticalLines(false);
    setIntercellSpacing(new Dimension(0, 1));

    sorter = new TableSorter(tm);
    setModel(sorter);
    sorter.addMouseListenerToHeaderInTable(this);

    firstRowIsTemp = false;
    continuouslyUpdateFirstRow = false;
    firstRowUpdateTimer =
	new javax.swing.Timer(1000, (ActionListener)AlloySwingUtilities.listener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    ((AbstractTableModel)tm).fireTableRowsUpdated(0, 0);
		}
	    }));
    canContinuouslyUpdate = new boolean[getColumnCount()];
    Arrays.fill(canContinuouslyUpdate, false);

    columnIsFixedWidth = new boolean[getColumnCount()];
    Arrays.fill(columnIsFixedWidth, false);

    columnClipping = new int[getColumnCount()];
    Arrays.fill(columnClipping, RIGHT_CLIPPING);

    addMouseListener((MouseListener)AlloySwingUtilities.listener(new MouseAdapter() {
                public void mousePressed(MouseEvent e) {
                    Point p = e.getPoint();
                    int col = columnAtPoint(p);
                    int row = rowAtPoint(p);

                    if (!(getValueAt(row, col) instanceof JMenu))
                        return;

                    JMenu menu = (JMenu)getValueAt(row, col);
                    Rectangle rect = getCellRect(row, col, true);

                    if (getColumnClass(col) == JMenu.class) {
                        menu.getPopupMenu().show(AlloyTable.this, rect.x + rect.width, rect.y);
                        menu.getPopupMenu().setSelected(null);
                    }
                }
            }));

    setDefaultRenderer(Object.class, new AlloyTableCellRenderer());
    
    setDefaultRenderer(Date.class, new AlloyTableCellRenderer() {
	    public void setValue(Object value) {
		setText((value == null) ? "" : dateFormatter.format(value));
	    }
	});
    
    setDefaultRenderer(AnalysisIcon.class, new TableCellRenderer() {
	    TableCellRenderer defaultRenderer =
		getDefaultRenderer(AnalysisIcon.class);
	    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
		Component renderer = defaultRenderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
		((JComponent)renderer).setToolTipText(((AnalysisIcon)value).getToolTipText());
		return renderer;
	    }
        });

    // create popup menu of all named columns
    final JPopupMenu columnMenu = new JPopupMenu();
    column = new TableColumn[getColumnCount()];
    columnMenuItems = new JMenuItem[getColumnCount()];
    columnIsVisible = new boolean[getColumnCount()];
    for (int i = 0; i < getColumnCount(); i++) {
        column[i] = getColumnModel().getColumn(i);
        columnIsVisible[i] = true;

        if (getColumnName(i) != null) {
        final JMenuItem columnMenuItem = new JCheckBoxMenuItem();
        final int modelIndex = i;
        columnMenuItem
            .setAction(AlloySwingUtilities.action(new AbstractAction(getColumnName(i)) {
                public void actionPerformed(ActionEvent e) {
                if (columnMenuItem.isSelected()) {
                    showColumn(modelIndex);
                }
                else {
                    hideColumn(modelIndex);
                }
                }
            }));
        columnMenuItem.setSelected(true);
        columnMenu.add(columnMenuItem);
        columnMenuItems[i] = columnMenuItem;
        }
    }
    getTableHeader()
        .addMouseListener((MouseListener)AlloySwingUtilities.listener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
            showPopup(e);
            }

            public void mouseClicked(MouseEvent e) {
            showPopup(e);
            }

            public void mouseReleased(MouseEvent e) {
            showPopup(e);
            }

            void showPopup(MouseEvent e) {
            if (e.isPopupTrigger())
                columnMenu.show(AlloyTable.this, e.getX(), e.getY());
            }
        }));
    getTableHeader().setMinimumSize(new Dimension(100, 100));

    setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

    sorter
        .addTableModelListener(
                   (TableModelListener)AlloySwingUtilities
                   .listener(new TableModelListener() {
                       public void tableChanged(TableModelEvent e) {
			   //resizeColumns();

                           // trigger selection event

                           int newRow = Math.min(getRowCount()-1, getSelectedRow());
                           getSelectionModel().clearSelection();
                           if (newRow >= 0)
                           setRowSelectionInterval(newRow, newRow);

                           revalidate(); repaint();
                       }
                       }));
    
    resizeColumns();
    }

    // windows "ugly font" stuff
    public void paintComponent(Graphics g) {
    if (onWindows) {
        //  feeble attempt to fix "ugly" fonts on windows
        if (g instanceof Graphics2D) {
        Graphics2D g2 = (Graphics2D)g;
        if (getFont().getSize() > 18) {
            g2.addRenderingHints(
                     new RenderingHints(
                                RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_ON));
        }
        else {
            g2.addRenderingHints(
                     new RenderingHints(
                                RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_OFF));
        }

        super.paintComponent(g2);
        g2.dispose();
        }
        else {
        super.paintComponent(g);
        }
    }
    else {
        super.paintComponent(g);
    }
    }

    public void setFont(Font f) {
    if (onWindows) {
        if (!f.equals(selectedFont)) {
        renderedFont = calculateRenderedFont(f);
        selectedFont = f;
        super.setFont(renderedFont);
        }
    }
    else {
        super.setFont(f);
    }

    ((DefaultCellEditor)getDefaultEditor(Object.class)).getComponent().setFont(f);
    }

    private Font calculateRenderedFont(Font f) {
    return new Font(
            f.getName(),
            Font.PLAIN,
            (int)Math.round(f.getSize() * windowsScaleFactor));
    }

    public void tableChanged(TableModelEvent e) {
    super.tableChanged(e);

    // workaround for Java Bug ID: 4700643
    // JTable selection misbehaves when a row is inserted at position 0

    if (e.getType() == TableModelEvent.INSERT) {
        removeRowSelectionInterval(e.getFirstRow(), e.getLastRow());
    }
    }

    /** Overrides JTable.setDefaultRenderer so that row 0 is displayed differently if firstRowIsTemp
     */
    public void setDefaultRenderer(Class columnClass, final TableCellRenderer renderer) {
    super.setDefaultRenderer(columnClass, new TableCellRenderer() {
                public Component getTableCellRendererComponent(
                                   JTable table,
                                   Object value,
                                   boolean isSelected,
                                   boolean hasFocus,
                                   int row,
                                   int column) {
                    Component c =
                        renderer.getTableCellRendererComponent(
                                   table,
                                   value,
                                   isSelected,
                                   hasFocus,
                                   row,
                                   column);
                    if (firstRowIsTemp && sorter.getRowModelIndexAt(row) == 0)
                        c.setFont(c.getFont().deriveFont(c.getFont().getStyle() ^ Font.BOLD));
                    return c;
                }
            });
    }

    boolean getFirstRowIsTemp() {
    return firstRowIsTemp;
    }

    void setFirstRowIsTemp(boolean b) {
    firstRowIsTemp = b;
    }

    /* Returns model index of selected row, which may be different than table index
     * if rows have been resorted.
     */
    int getSelectedRowModelIndex() {
    return sorter.getRowModelIndexAt(getSelectedRow());
    }

    void setSelectedRowModelIndex(int i) {
    int index = sorter.indexOfRowModelIndex(i);
    setRowSelectionInterval(index, index);
    }

    void showColumn(int i) {
    int insertIndex = 0;
    for (int j = 0; j < i; j++) {
        if (columnIsVisible[j])
        insertIndex++;
    }
    addColumn(column[i]);
    moveColumn(getColumnCount() - 1, insertIndex);
    columnIsVisible[i] = true;
    columnMenuItems[i].setSelected(true);
    //if (getRowCount() == 0)
    //columnsNeedResizing = true;
    continuouslyUpdateFirstRow(continuouslyUpdateFirstRow);
    }

    void showColumn(String s) {
    showColumn(getColumnIndex(s));
    }

    void hideColumn(int i) {
    removeColumn(column[i]);
    columnIsVisible[i] = false;
    columnMenuItems[i].setSelected(false);
    //if (getRowCount() == 0)
    //columnsNeedResizing = true;
    continuouslyUpdateFirstRow(continuouslyUpdateFirstRow);
    }

    void hideColumn(String s) {
    hideColumn(getColumnIndex(s));
    }

    boolean isColumnShown(int i) {
    return columnIsVisible[i];
    }

    boolean isColumnShown(String s) {
    return isColumnShown(getColumnIndex(s));
    }

    void setColumnClipping(int col, int clipping) {
    columnClipping[col] = clipping;
    }

    void setColumnClipping(String s, int clipping) {
    setColumnClipping(getColumnIndex(s), clipping);
    }

    int getColumnClipping(int col) {
    return columnClipping[col];
    }

    int getColumnClipping(String s) {
    return getColumnClipping(getColumnIndex(s));
    }

    int getColumnIndex(String s) {
	for (int i=0; i<column.length; i++) {
	    if (column[i].getIdentifier().equals(s))
		return i;
	}
	return -1;
    }

    // set whether col will continuously update when
    // continuouslyUpdateFirstRow is true
    void setCanContinuouslyUpdateColumn(int col, boolean b) {
	canContinuouslyUpdate[col] = b;
    }

    void setCanContinuouslyUpdateColumn(String s, boolean b) {
	setCanContinuouslyUpdateColumn(getColumnIndex(s), b);
    }

    void continuouslyUpdateFirstRow(boolean b) {
	continuouslyUpdateFirstRow = b;

	boolean shouldRun = false;
	if (b) {
	    for (int i=0; i<column.length; i++) {
		if (canContinuouslyUpdate[i] && columnIsVisible[i]) {
		    shouldRun = true;
		    break;
		}
	    }
	}
	if (shouldRun && !firstRowUpdateTimer.isRunning()) {
	    firstRowUpdateTimer.start();
	} else if (!shouldRun && firstRowUpdateTimer.isRunning()) {
	    firstRowUpdateTimer.stop();
	}
    }

    void setColumnIsFixedWidth(int col, boolean b) {
	columnIsFixedWidth[col] = b;
	resizeColumns();
    }

    void setColumnIsFixedWidth(String s, boolean b) {
	setColumnIsFixedWidth(getColumnIndex(s), b);
    }

    private void resizeColumns() {
	for (int i = 0; i < getColumnCount(); i++) {
	    TableColumn tc = getColumnModel().getColumn(i);

	    if (columnIsFixedWidth[i]) {
		tc.setMinWidth(getPreferredWidthForColumn(tc));
		tc.setPreferredWidth(getPreferredWidthForColumn(tc));
		tc.setMaxWidth(getPreferredWidthForColumn(tc));
	    } else {
		tc.setPreferredWidth(getPreferredWidthForColumn(tc));
	    }
	}
    }

    private int getPreferredWidthForColumn(TableColumn col) {
    int hw = columnHeaderWidth(col) + 3, // hw = header width
        cw = widestCellInColumn(col); // cw = column width

    return (hw > cw ? hw : cw) + 3;
    }

    private int columnHeaderWidth(TableColumn col) {
    TableCellRenderer renderer = col.getHeaderRenderer();
    if (renderer == null)
        renderer = getTableHeader().getDefaultRenderer();

    Component comp =
        renderer.getTableCellRendererComponent(
                           this,
                           col.getHeaderValue(),
                           false,
                           false,
                           0,
                           0);

    return comp.getPreferredSize().width;
    }

    private int widestCellInColumn(TableColumn col) {
    int c = col.getModelIndex(), width = 0, maxw = 0;

    for (int r = 0; r < getRowCount(); ++r) {
        TableCellRenderer renderer = col.getCellRenderer();
        if (renderer == null)
        renderer = getDefaultRenderer(getModel().getColumnClass(c));

        Component comp =
        renderer.getTableCellRendererComponent(
                               this,
                               getModel().getValueAt(r, c),
                               false,
                               false,
                               r,
                               c);

        width = comp.getPreferredSize().width;
        maxw = width > maxw ? width : maxw;
    }
    return maxw;
    }
}


class AlloyTableCellRenderer extends DefaultTableCellRenderer {
    private int preferredWidth;

    public Component getTableCellRendererComponent(JTable table,
                           Object value,
                           boolean isSelected,
                           boolean hasFocus,
                           int row,
                           int column) {
        int width;
        int swidth;
        int vwidth;
        int i = 0;
        FontMetrics fm;
        Insets insets;
    int clipping = ((AlloyTable)table).getColumnClipping(column);
        String strValue = (value == null) ? "" : value.toString();
        int strValueLen = strValue.length();
        View view = null;
        super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
        if (strValue.length() == 0) {
            return this;
        }

        width = table.getCellRect(row, column, false).width;
        // Remove inset width
        insets = this.getInsets();
        width -= insets.left + insets.right;


        // Handle cells with HTML content by simply determining if the tooltip text should be set
        view = (View) getClientProperty("html");
        if (view != null) {
            vwidth = (int) view.getPreferredSpan(View.X_AXIS);
            if (width > 0 && vwidth > width) {
                setToolTipText(strValue);
            } else {
                setToolTipText(null);
            }
            return this;
        }

        fm = getFontMetrics(this.getFont());
        swidth = fm.stringWidth(strValue);
	preferredWidth = swidth;
        if (width > 0 && swidth > width) {
            while (swidth > width && i < strValueLen) {
                i += 1;
                String test;
                if (clipping == AlloyTable.LEFT_CLIPPING) {
                    test = "..." + strValue.substring(i);
                } else {
                    test = strValue.substring(i, strValue.length()) + "...";
                }
                swidth = fm.stringWidth(test);
            }
            this.setToolTipText(strValue);
            if (clipping == AlloyTable.LEFT_CLIPPING) {
                strValue = "..." + strValue.substring(i);
            } else {
                strValue = strValue.substring(0, strValue.length()) + "...";
            }
        } else {
            this.setToolTipText(null);
        }
        setText(strValue);
        return this;

    }

    public Dimension getPreferredSize() {
	Dimension size = super.getPreferredSize();
	size.width = preferredWidth;
	return size;
    }
}

